package fr.miage.course7.gps;

import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TreeSet;

import fr.miage.course7.gps.Participant.Categorie;

/**
 * Generateur de la course.
 * @author Mickael LAROCHE
 *
 */
public class CourseGenerateur {

    /**
     * Liste des participants.
     */
    private HashMap<Integer, Participant> participants;

    /**
     * Liste des étapes.
     */
    private ArrayList<Position> etapes;

    /**
     * Itinéraire.
     */
    private ArrayList<Position> itineraire;

    /**
     * Bornes des coordonnees interdites.
     */
    private ArrayList<TreeSet<Integer>> coordonneesInterdites;

    /*
     * Nombre d'étapes.
     */
    //private int nbEtapes = 3;//utile uniquement si generation aleatoire

    /**
     * Nombre de participants.
     */
    private final int nbParticipants = 3;

    /**
     * Latitude maximale.
     */
    private final int latMax = 90;

    /**
     * Latitude minimale.
     */
    private final int latMin = -90;

    /**
     * Longitude maximale.
     */
    private final int lonMax = 180;

    /**
     * Longitude minimale.
     */
    private final int lonMin = -180;

    /**
     * Converti un entier en categorie.
     * @param i l'entier a convertir (entre 0 et 2)
     * @return la categorie correspondant a l'entier
     */
    private Categorie intToCat(final int i) {
        switch (i) {
            case 0 :
                return Participant.Categorie.catamaran;
            case 1 :
                return Participant.Categorie.monocoque;
            case 2 :
                return Participant.Categorie.trimaran;
            default :
                System.err.println("intToCat: mauvais entier."
                                 + "Utilisation d'une valeur par defaut");
                return Participant.Categorie.catamaran;
        }
    }

    /**
     * Constructeur demarrant la course.
     * Demarre a partir des extremites des coordonnees interdites,
     * des etapes, et de l'itineraire propose
     * @param coordInterdites les extremites des coordonnees interdites
     */
    public CourseGenerateur(final ArrayList<TreeSet<Integer>> coordInterdites,
                            final ArrayList<Position> etapes,
                            final ArrayList<Position> itineraire) {
        this.etapes = etapes;
        this.itineraire = itineraire;

        final int nbCategories = 3;

        Position depart = itineraire.get(0);

        this.coordonneesInterdites = coordInterdites;
        participants = new HashMap<Integer, Participant>();

        //definition de quelques participants
        for (int i = 1; i <= nbParticipants; ++i) {
            nouveauParticipant(i,
                    intToCat((int) (Math.random() * nbCategories)),
                    "Bateau" + i,
                    "Skippeur" + i);
        }

//        //enregistrement alatoire des etapes
//        for (int i=0;i<nbEtapes;++i) {
//            etapes.add(choisirPortAleatoire());
//        }

      //affichage de l'itineraire
        for (int i = 0; i < itineraire.size(); ++i) {
            System.out.println(i + " : " + itineraire.get(i));
        }

        //affichage des etapes
        for (int i = 0; i < etapes.size(); ++i) {
            System.out.println(i + " : " + etapes.get(i));
        }

        //affichage des participants
        for (int i = 1; i <= participants.size(); ++i) {
            System.out.println(participants.get(i));
        }

        //1ere transmission des participants
        for (int i = 1; i <= participants.size(); ++i) {
            participants.get(i)
                        .nouvelleTransmission(new GregorianCalendar(),
                                              depart.getLat(),
                                              depart.getLon(),
                                              1
                                             );
        }

        //re-affichage des participants
        for (int i = 1; i <= participants.size(); ++i) {
            System.out.println(participants.get(i));
        }

        //transmission des participants suivante
        nouvelleTransmissionAleatoire();

        //re-re-affichage des participants
        for (int i = 1; i <= participants.size(); ++i) {
            System.out.println(participants.get(i));
        }
    }

    /**
     * Constructeur demarrant la course.
     * Demarre a partir des extremites des coordonnees interdites
     * @param coordInterdites les extremites des coordonnees interdites
     */
    public CourseGenerateur(final ArrayList<TreeSet<Integer>> coordInterdites) {

        final int nbCategories = 3;

        this.coordonneesInterdites = coordInterdites;
        participants = new HashMap<Integer, Participant>();

        //definition de quelques participants
        for (int i = 1; i <= nbParticipants; ++i) {
            nouveauParticipant(i,
                    intToCat((int) (Math.random() * nbCategories)),
                    "Bateau" + i,
                    "Skippeur" + i);
        }

        etapes = new ArrayList<Position>();
        //definition de quelques coordonnees d'etapes
        //definition de quelques coordonnees d'etapes
        Position depart = nouvelleEtape(+46, -5);
        Position etape = nouvelleEtape(-60, -75);
        Position arrivee = nouvelleEtape(-40, +16);

        itineraire = new ArrayList<Position>();

        //definition de l'itineraire correspondant
        itineraire.add(depart);
        itineraire.add(new Position(+46, -35));
        itineraire.add(new Position(-10, -25));
        itineraire.add(new Position(-40, -40));
        itineraire.add(etape);
        itineraire.add(new Position(-55, -120));
        itineraire.add(new Position(-55, -180));
        itineraire.add(new Position(-53, 140));
        itineraire.add(new Position(-50, 90));
        itineraire.add(new Position(-48, 30));
        itineraire.add(arrivee);

        //affichage des etapes
        for (int i = 0; i < etapes.size(); ++i) {
            System.out.println(i + " : " + etapes.get(i));
        }

        //affichage des participants
        for (int i = 1; i <= participants.size(); ++i) {
            System.out.println(participants.get(i));
        }

        //1ere transmission des participants
        for (int i = 1; i <= participants.size(); ++i) {
            participants.get(i)
                        .nouvelleTransmission(new GregorianCalendar(),
                                              depart.getLat(),
                                              depart.getLon(),
                                              1
                                             );
        }

        //transmission des participants suivante
        nouvelleTransmissionAleatoire();
    }

    /**
     * Cree une nouvelle etape et l'ajoute a la liste.
     * @param lat la latitude de l'etape
     * @param lon la longitude de l'etape
     * @return l'etape creee
     */
    private final Position nouvelleEtape(final int lat, final int lon) {
        Position temp = new Position(lat, lon);
        etapes.add(temp);
        return temp;
    }

    /**
     * Retourne une latitude entre latMin et latMax.
     * @param lat la latitude souhaitee
     * @return la latitude autorisee la plus proche de lat
     */
    private final int nouvLat(final int lat) {
        return Math.max(Math.min(lat, latMax), latMin);
    }

    /**
     * Retourne une longitude entre lonMin et lonMax.
     * @param lon la longitude souhaitee
     * @return la longitude autorisee la plus proche de lon
     */
    private final int nouvLon(final int lon) {
        if (lon < lonMin) {
            return lonMax - (lonMin - lon);
        } else if (lon > lonMax) {
            return lonMin + (lon - lonMax);
        } else {
            return lon;
        }
    }

    /**
     * Genere et enregistre une transmission aleatoire pour chaque participant.
     */
    public final void nouvelleTransmissionAleatoire() {

        for (int i = 1; i <= participants.size(); ++i) {

            Position prochaineEtape = itineraire.get(participants.get(i)
                                                       .getDerniereEtape() + 1);
            Position posActuelle = participants.get(i).getDernierePosition();
            Position prochainePosition = null;
            boolean sud = (prochaineEtape.getLat() < posActuelle.getLat());
            boolean ouest = (prochaineEtape.getLon() < posActuelle.getLon()
                       || prochaineEtape.getLon() - posActuelle.getLon() > 180);
            boolean changementCote = Math.abs(prochaineEtape.getLon()
                                            - posActuelle.getLon()) > 180;
            int distance = (int) (1 + Math.random() * 4);
            int angle = ((int) (Math.random() * 5)) - 2;

            float coeffDirecteur = coeffDirecteur(posActuelle, prochaineEtape);
            if (coeffDirecteur == 0) {
                //System.out.println("CD0");
                // si la distance choisie est superieure a la distance au point,
                // on la diminue
                if (distance > pseudoDistance(prochaineEtape, posActuelle)) {
                    distance = (int) pseudoDistance(prochaineEtape,
                                                    posActuelle);
                }
                if (sud) {
                    prochainePosition = new Position(
                                      nouvLat(posActuelle.getLat() - distance),
                                      posActuelle.getLon()
                                      );
                } else {
                    prochainePosition = new Position(
                                      nouvLat(posActuelle.getLat() + distance),
                                      posActuelle.getLon()
                                      );
                }
            } else if (coeffDirecteur == Float.MIN_VALUE) {
                //System.out.println("CD%MV");

                // si la distance choisie est superieure a la distance au point,
                // on la diminue
                if (distance > pseudoDistance(prochaineEtape, posActuelle)) {
                    distance = (int) pseudoDistance(prochaineEtape,
                                                    posActuelle);
                }
                if (ouest) {
                    prochainePosition = new Position(
                                       posActuelle.getLat(),
                                       nouvLon(posActuelle.getLon() - distance)
                                       );
                } else {
                    prochainePosition = new Position(
                                       posActuelle.getLat(),
                                       nouvLon(posActuelle.getLon() + distance)
                                       );
                }
            } else if (sud) { //il faut aller au sud
                //System.out.println("SUD");
                boolean pointValide = false;
                for (int j = 1; j < distance; ++j) {
                    Position prochainPoint;
                    if (changementCote) {
                         if (ouest) {
                         prochainPoint = new Position(
                                nouvLat(posActuelle.getLat() - j),
                                nouvLon((int) (posActuelle.getLon() - j))
                                );
                         } else {
                             prochainPoint = new Position(
                                     nouvLat(posActuelle.getLat() - j),
                                     nouvLon((int) (posActuelle.getLon() + j))
                                     );
                         }
                    } else {
                        if (ouest) {
                        prochainPoint = new Position(
                                nouvLat(posActuelle.getLat() - j),
                                nouvLon((int) (posActuelle.getLon()
                                              - j / 2 * coeffDirecteur))
                                );
                        } else {
                            prochainPoint = new Position(
                                    nouvLat(posActuelle.getLat() - j),
                                    nouvLon((int) (posActuelle.getLon()
                                                  + j / 2 * coeffDirecteur))
                                    );
                        }
                    }

                    if (Math.floor(pseudoDistance(posActuelle,
                                                  prochainPoint)
                                                 ) <= distance) {
                        if (coordonneeAutorisee(prochainPoint)) {
                            prochainePosition = prochainPoint;
                            pointValide = true;
                            if (prochainePosition.equals(prochaineEtape)
                               || pseudoDistance(prochaineEtape,
                                                 prochainePosition
                                                ) <= 1
                                ) {
                                break;
                            }
                        }
                    } else {
                        break;
                    }
                }
                if (!pointValide) {
                    for (int k = -5; k < 5; ++k) {
                        for (int l = -5; l < 5; ++l) {
                            Position prochainPoint
                                = new Position(
                                             nouvLat(posActuelle.getLat() + k),
                                             nouvLon(posActuelle.getLon() + l)
                                              );

                            if (coordonneeAutorisee(prochainPoint)) {
                                prochainePosition = prochainPoint;
                                pointValide = true;
                                break;
                            }
                        }
                        if (pointValide) {
                            break;
                        }
                    }
                    if (!pointValide) {
                        System.err.println("Pas de point valide trouvé...");
                        System.exit(1);
                    }
                }
            } else { //il faut aller au nord
                //System.out.println("NORD");
                boolean pointValide = false;
                for (int j = 1; j < distance; ++j) {
                    Position prochainPoint;
                    if (changementCote) {
                         if (ouest) {
                         prochainPoint = new Position(
                                nouvLat(posActuelle.getLat() + j),
                                nouvLon((int) (posActuelle.getLon() - j))
                                );
                         } else {
                             prochainPoint = new Position(
                                     nouvLat(posActuelle.getLat() + j),
                                     nouvLon((int) (posActuelle.getLon() + j))
                                     );
                         }
                    } else {
                        if (ouest) {
                        prochainPoint = new Position(
                                nouvLat(posActuelle.getLat() + j),
                                nouvLon((int) (posActuelle.getLon()
                                              + j / 2 * coeffDirecteur))
                                );
                        } else {
                            prochainPoint = new Position(
                                    nouvLat(posActuelle.getLat() + j),
                                    nouvLon((int) (posActuelle.getLon()
                                                  - j / 2 * coeffDirecteur))
                                    );
                        }
                    }

                    if (Math.floor(pseudoDistance(posActuelle,
                                                  prochainPoint)
                                                 ) <= distance) {
                        if (coordonneeAutorisee(prochainPoint)) {
                            prochainePosition = prochainPoint;
                            pointValide = true;
                            if (prochainePosition.equals(prochaineEtape)
                                    || pseudoDistance(prochaineEtape,
                                                      prochainePosition
                                                     ) <= 1
                               ) {
                                break;
                            }
                        }
                    } else {
                        break;
                    }
                }
                if (!pointValide) {
                    for (int k = -5; k < 5; ++k) {
                        for (int l = -5; l < 5; ++l) {
                            Position prochainPoint =
                                new Position(
                                             nouvLat(posActuelle.getLat() + k),
                                             nouvLon(posActuelle.getLon() + l)
                                             );
                            if (coordonneeAutorisee(prochainPoint)) {
                                prochainePosition = prochainPoint;
                                pointValide = true;
                                break;
                            }
                        }
                        if (pointValide) {
                            break;
                        }
                    }
                    if (!pointValide) {
                        System.err.println("Pas de point valide trouvé...");
                        System.exit(1);
                    }
                }
            }

            // si la distance entre la prochaine position et la prochaine etape
            // est inferieure a 1, on peut supposer un arrondi, qu'on corrige
            if (pseudoDistance(prochaineEtape, prochainePosition) <= 2) {
                prochainePosition = prochaineEtape;
            }
            int incEtape = 0;
            // si la prochaine etape est atteinte par la prochaine position,
            // on incremente la derniere etape atteinte chez le participant
            if (prochainePosition.equals(prochaineEtape)) {
                incEtape = 1;
            } else { // sinon, s'il ne s'agit pas d'une etape obligatoire
                if (!etapes.contains(prochaineEtape)) {
                    // si on est proche d'une etape de l'itineraire
                    // non obligatoire, on peut passer a la prochaine
                    System.out.println("xxx=" + pseudoDistance(prochaineEtape,
                                                              prochainePosition)
                                                              );
                    if (pseudoDistance(prochaineEtape,
                                       prochainePosition
                                      ) < 10) {
                        incEtape = 1;
                    } else { // sinon, on ajoute l'angle pour un aleatoire
                             // de trajectoire, en verifiant sa correction
                        prochainePosition.setLat(
                                    nouvLat(prochainePosition.getLat() + angle)
                                                );
                        prochainePosition.setLon(
                                    nouvLon(prochainePosition.getLon() + angle)
                                                );
                        if (!coordonneeAutorisee(prochainePosition)) {
                            prochainePosition.setLat(
                                    nouvLat(prochainePosition.getLat() - angle)
                                                      );
                            prochainePosition.setLon(
                                    nouvLon(prochainePosition.getLon() - angle)
                                                    );
                        }
                    }
                }
            }
            GregorianCalendar derniereDate =  participants.get(i)
                                                          .getDerniereDate();
            GregorianCalendar prochaineDate
                = new GregorianCalendar(
                            derniereDate.get(GregorianCalendar.YEAR),
                            derniereDate.get(GregorianCalendar.MONTH),
                            derniereDate.get(GregorianCalendar.DAY_OF_MONTH) + 1
                                        );

            participants.get(i)
                .nouvelleTransmission(prochaineDate,
                                      prochainePosition.getLat(),
                                      prochainePosition.getLon(),
                                      incEtape
                                     );
        }
    }

    /**
     * Calcule le coefficient directeur entre les positions c1 et c2.
     * @param c1 la position 1
     * @param c2 la position 2
     * @return le coefficient directeur (float) entre les positions c1 et c2
     *         ou Float.MIN_VALUE s'il n'existe pas
     */
    public final float coeffDirecteur(final Position c1, final Position c2) {
        if (c2.getLat() == c1.getLat()) {
            return Float.MIN_VALUE;
        } else {
            return (float) (c2.getLon() - c1.getLon())
                 / (float) (c2.getLat() - c1.getLat());
        }
    }

    /**
     * Indique si la position c est hors des continents.
     * @param c la position a tester
     * @return vrai si la position c est en mer, faux sinon
     */
    public final boolean coordonneeAutorisee(final Position c) {
        TreeSet<Integer> lons
                      = coordonneesInterdites.get(Serveur.latToInd(c.getLat()));
        Iterator<Integer> it = lons.iterator();
        while (it.hasNext()) {
            int l1 = it.next();
            int l2 = it.next();
            if (c.getLon() > l1 && c.getLon() < l2) {
                return false;
            }
        }
        return true;
    }

    /**
     * Calcule une distance entre les positions c1 et c2.
     * Cette distance est proportionnelle a l'affichage
     * @param c1 la premiere position
     * @param c2 la seconde position
     * @return une distance proportionnelle a l'affichage (double)
     *         entre les positions c1 et c2
     */
    public final double pseudoDistance(final Position c1, final Position c2) {
        if (c1 != null && c2 != null) {
            return Math.sqrt(
                    (c2.getLat() - c1.getLat()) * (c2.getLat() - c1.getLat())
                  + (c2.getLon() - c1.getLon()) * (c2.getLon() - c1.getLon())
                             );
        } else {
            System.err.println("Position 1 : " + c1);
            System.err.println("Position 2 : " + c2);
            System.err.println("Position nulle pour le calcul d'une distance");
            return -1;
        }
    }

    /**
     * Cree un nouveau participant et l'ajoute a la liste.
     * @param id l'identifiant du participant
     * @param c sa categorie
     * @param bat le nom de son bateau
     * @param skip le nom du skippeur
     * @return le participant cree
     */
    public final Participant nouveauParticipant(final int id,
                                                final Categorie c,
                                                final String bat,
                                                final String skip) {
        Participant temp = new Participant(id, c, bat, skip);
        participants.put(id, temp);
        return temp;
    }

    /**
     * Retourne les coordonnées d'un "port" (point sur la cote) aleatoire.
     * @return les coordonnées d'un "port" (point sur la cote) aleatoire
     */
    public final Position choisirPortAleatoire() {
        boolean valide = false;
        int portIndLat = 0, portLat = 0;

        while (!valide) {
            portIndLat = (int) (Math.random() * 181);
            if (coordonneesInterdites.get(portIndLat).size() != 0) {
                portLat = Serveur.indToLat(portIndLat);
                valide = true;
            }
        }
        int portIndLon = (int) (Math.random()
                                * coordonneesInterdites.get(portIndLat).size()
                               );

        int portLon = (Integer) (coordonneesInterdites.get(portIndLat).toArray()
                                )[portIndLon];

        return new Position(portLat, portLon);
    }

    /**
     * Retourne les participants de la course.
     * @return les participants de la course
     *         sous forme de HashMap<Integer, Participant>
     */
    public final HashMap<Integer, Participant> getParticipants() {
        return participants;
    }

}
